package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/format"
	"go/token"
	"go/types"
	"golang.org/x/tools/go/packages"
	"log"
	"os"
	"path/filepath"
	"strings"
)

func main() {
	initParams()
	// flag.Usage 是一个函数类型的变量，它定义了如何输出在用户输入无效参数时应该显示的消息
	flag.Usage = Usage
	// 解析命令行参数
	flag.Parse()
	fmt.Println(param.String())
	if len(param.fileNames) == 0 {
		flag.Usage()
		os.Exit(2)
	}
	files := strings.Split(param.fileNames, ",")
	// 转成相对路径
	files = TransToRedirectPaths(files)
	var tags []string
	if len(param.buildTags) > 0 { // 只有携带了 tag 参数的文件才能编译运行	如 这样 // +build jsoniter
		tags = strings.Split(param.buildTags, ",")
	}
	// 只需解析一次包
	var dir string
	g := Generator{}
	if len(files) == 1 && isDirectory(files[0]) { // 如果只有一个字符串 并且是目录
		dir = files[0]
	} else {
		dir = filepath.Dir(files[0]) // 尝试 返回上层目录
	}
	fmt.Println(dir)
	g.parsePackage(files, tags) // args:目录  tags:应用的构建标记 // 对于每个类型运行生成代码
	for _, file := range files {
		g.generate(file)
	}
	var src []byte // 输出流临时存放
	// Format the output.
	src = g.format()
	// Write to file.
	outputName := param.output // 写入的文件路径
	if outputName == "" {      // 如果为空 则创建一个文件
		// strings.ReplaceAll:替换所有 	Base:返回path的最后一个元素
		baseName := fmt.Sprintf("%s_init_registry.go", strings.ReplaceAll(filepath.Base(g.pkg.name), "-", "_"))

		outputName = filepath.Join(dir, strings.ToLower(baseName))
	}
	err := os.WriteFile(outputName, src, 0600)
	if err != nil {
		log.Fatalf("writing output: %s", err)
	}
}

var (
	param = Param{
		fileNames: "",
		output:    "",
		buildTags: "",
		must:      true,
	}
)

type Param struct {
	fileNames string
	output    string
	buildTags string
	must      bool
}

func (p *Param) String() string {
	return fmt.Sprintf("fileNames: %s \noutput: %s \nbuildTags: %s \nmust: %t \n", p.fileNames, p.output, p.buildTags, p.must)
}

func initParams() {
	flag.StringVar(&param.fileNames, "n", "", "需要生成的code的文件名称，多个用(,)隔开，必填字段")
	flag.StringVar(&param.output, "o", "", "输出路径，默认为格式为：默认名称_init_registry.go")
	flag.StringVar(&param.buildTags, "t", "", "应用的一组逗号分隔的构建标签")
	flag.BoolVar(&param.must, "m", true, "是否必须注册成功，true调用MustRegistry方法")

}

func Usage() {
	fmt.Fprintf(os.Stderr, "Usage of codegen:\n")
	fmt.Fprintf(os.Stderr, "\tcode_init_gen [flags] -name T [directory]\n")
	fmt.Fprintf(os.Stderr, "\tcode_init_gen [flags] -name T files... # Must be a single package\n")
	fmt.Fprintf(os.Stderr, "Flags:\n")

}

// isDirectory 报告命名文件是否为目录
func isDirectory(name string) bool {
	info, err := os.Stat(name)
	if err != nil {
		log.Fatal(err)
	}

	return info.IsDir() // 是否是 目录
}

// Generator 结构体用于保存代码分析状态。主要用于缓存输出结果以便通过 format.Source 进行格式化。
type Generator struct {
	buf bytes.Buffer // 积累输出 output.
	pkg *Package     // Package 扫描包
}

// parsePackage 函数分析由给定的模式和标记构造的单个包
// 如果出现错误，parsePackage 函数将终止程序
func (g *Generator) parsePackage(patterns []string, tags []string) {
	cfg := &packages.Config{
		// packages.NeedSyntax：在加载包的元数据的基础上，还会加载包的源码文件，并解析成语法树
		// packages.NeedTypes：在加载包的源码文件的基础上，还会对其进行类型检查，生成包的类型信息
		// packages.NeedTypesInfo | packages.NeedImports | packages.NeedDeps: 加载完整的类型信息，包名信息和所有直接/间接依赖的包信息
		Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypes | packages.NeedImports | packages.NeedDeps | packages.NeedName,
		// in a separate pass? For later.
		Tests:      false,                                                      // 是否加载测试文件  默认值为 false 不加载
		BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, //
	}
	pkgs, err := packages.Load(cfg, patterns...) // 函数返回的信息是一个 []*packages.Package 类型的切片，其中每个元素都表示一个被分析的包。每个 *packages.Package 类型的对象包含了对应包的语法树、类型信息、依赖关系等各种信息
	if err != nil {
		log.Fatal(err)
	}
	if len(pkgs) != 1 {
		log.Fatalf("error: %d packages found", len(pkgs))
	}
	g.addPackage(pkgs[0]) // 获取 当前 main.go 解析的代码内容
}

// addPackage 函数将一个类型检查后的 Package 和它的语法文件添加到生成器中
func (g *Generator) addPackage(pkg *packages.Package) {
	g.pkg = &Package{
		name:  pkg.Name,                       // 包名 code
		defs:  pkg.TypesInfo.Defs,             // pkg.TypesInfo 包的类型信息
		files: make([]*File, len(pkg.Syntax)), // pkg.Syntax 包下的的语法树
	}

	for i, file := range pkg.Syntax { // 遍历此包下的语法树 一个文件生成一个语法树
		g.pkg.files[i] = &File{
			file: file,  // 此语法树 所有内容 	指针
			pkg:  g.pkg, // 此包 所有内容		指针
		}
	}
}

// generate 会为指定的类型生成注册函数的代码
func (g *Generator) generate(typeName string) {
	values := make([]string, 0, 100)
	imports := make([]string, 0)
	for _, file := range g.pkg.files { // 在 ast.File 上做了一层封装  遍历 pkg 下的所有 语法树
		log.Printf("find file %s %d", file.file.Name, file.file.Name.NamePos)
		// 设置本次 walker 的状态
		file.typeName = typeName
		file.values = nil
		if file.file != nil { // 判断是否有语法树
			ast.Inspect(file.file, file.genDecl)
			values = append(values, file.values...)
			imports = append(imports, file.imports...)
		}
	}

	if len(values) == 0 {
		log.Fatalf("no values defined for type %s", typeName)
	}
	// Generate code
	g.Printf("// Code generated by \"codegen %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
	g.Printf("\n")
	g.Printf("package %s", g.pkg.name)
	g.Printf("\n")

	if len(imports) > 1 {
		g.Printf("import (")
		for _, s := range imports {
			g.Printf("%s\n", s)
		}
		g.Printf(")")
	}
	if len(imports) == 1 {
		g.Printf("import %s\n", imports[0])
	}
	g.Printf("\n")
	g.Printf("\n")

	g.Printf("\t// init register error codes defines in this source code to `czc/pkg/errors`\n")
	g.Printf("func init() {\n")
	for _, v := range values {
		if param.must {
			g.Printf("\tcode.MustRegister(%s)\n", v)
		} else {
			g.Printf("\tcode.Register(%s)\n", v)
		}

	}
	g.Printf("}\n")
}

// Printf like fmt.Printf, but add the string to g.buf.
// Printf 将格式化后的字符串写入 g.buf.
func (g *Generator) Printf(format string, args ...interface{}) {
	fmt.Fprintf(&g.buf, format, args...)
}

// format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) format() []byte {
	// 其作用是对 Go 语言的源代码进行格式化，并返回格式化后的源代码
	src, err := format.Source(g.buf.Bytes())
	if err != nil {
		// Should never happen, but can arise when developing this code.
		// The user can compile the output to see the error.
		log.Printf("warning: internal error: invalid Go generated: %s", err)
		log.Printf("warning: compile the package to analyze the error")

		return g.buf.Bytes()
	}

	return src
}

// Package defines options for package.
type Package struct {
	name  string
	defs  map[*ast.Ident]types.Object
	files []*File
}

// File holds a single parsed file and associated data.
type File struct {
	pkg  *Package  // Package to which this file belongs.
	file *ast.File // Parsed AST.
	// These fields are reset for each type being generated.
	typeName string // Name of the constant type.
	imports  []string
	values   []string // Accumulator for constant values of that type.
}

// nolint: gocognit
// genDecl processes one declaration clause.
func (f *File) genDecl(node ast.Node) bool {
	// 断言成imports
	imps, ok := node.(*ast.ImportSpec)
	if ok {
		f.imports = append(f.imports, imps.Path.Value)
		return true
	}

	decl, ok := node.(*ast.GenDecl)   // 断言是 GenDecl
	if !ok || decl.Tok != token.VAR { // 并且判断是否是 const 类型
		// We only care about const declarations.
		return true
	}
	for _, spec := range decl.Specs { // 遍历 decl 中的 元素  即 decl.Specs
		vspec, _ := spec.(*ast.ValueSpec) // 保证成功，因为这是CONST
		// 如果 元素类型为 空 且 有值的情况下
		if vspec.Type == nil && len(vspec.Values) > 0 {
			for _, name := range vspec.Names {
				f.values = append(f.values, name.Name)
			}
		}
	}
	return false
}

// GetCurrentDirectory 获取当前执行文件的路径
func GetCurrentDirectory() string {
	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		fmt.Printf("get path error. %v", err)
		return dir
	}
	return strings.Replace(dir, "\\", "/", -1) + "/"
}

func TransToRedirectPaths(paths []string) []string {
	if len(paths) == 0 {
		return []string{GetCurrentDirectory()}
	}
	redirectPath := make([]string, 0, len(paths))
	for _, path := range paths {
		if path == "." {
			redirectPath = append(redirectPath, GetCurrentDirectory())
			continue
		}
		if strings.HasPrefix(path, "./") {
			path = strings.Replace(path, "./", GetCurrentDirectory(), 1)
			redirectPath = append(redirectPath, path)
		}
	}
	return redirectPath
}
